/*
 *
 *  *    Copyright 2020-2021 luter.me
 *  *
 *  *    Licensed under the Apache License, Version 2.0 (the "License");
 *  *    you may not use this file except in compliance with the License.
 *  *    You may obtain a copy of the License at
 *  *
 *  *      http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  *    Unless required by applicable law or agreed to in writing, software
 *  *    distributed under the License is distributed on an "AS IS" BASIS,
 *  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  *    See the License for the specific language governing permissions and
 *  *    limitations under the License.
 *
 */

package com.luter.heimdall.plugins.redis.store;

import com.luter.heimdall.core.authorization.store.AuthorizationStore;
import com.luter.heimdall.core.config.ConfigManager;
import com.luter.heimdall.core.config.HeimdallProperties;
import com.luter.heimdall.core.details.UserDetails;
import com.luter.heimdall.core.exception.HeimdallCacheException;
import com.luter.heimdall.core.exception.HeimdallExceededTokenException;
import com.luter.heimdall.core.exception.HeimdallTokenException;
import com.luter.heimdall.core.listener.AbstractTokenStoreEvent;
import com.luter.heimdall.core.token.PageModel;
import com.luter.heimdall.core.token.SimpleToken;
import com.luter.heimdall.core.token.store.TokenStore;
import com.luter.heimdall.core.utils.StrUtils;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.slf4j.Logger;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static org.slf4j.LoggerFactory.getLogger;

/**
 * redis token store 实现
 * <p>
 * 注意：
 * <p>
 * 1、redis 自己会清理过期的 token,所以不需要定时任务来清理
 * <p>
 * 2、性能原因，某些批量操作，数据里大的情况下慎用，比如：删除、获取 所有活动 tokens
 *
 * @author luter
 */
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true, fluent = true)
public class RedisTokenStore extends AbstractTokenStoreEvent implements TokenStore {

    /**
     * The constant log.
     */
    private static final transient Logger log = getLogger(RedisTokenStore.class);
    /**
     * Token 缓存
     */
    private RedisTemplate<String, SimpleToken> tokenCache;

    /**
     * Token 删除后，同步删除用户的权限缓存，权限缓存也会被定时清理任务清理
     */
    private AuthorizationStore authorizationStore;

    /**
     * Instantiates a new Redis token store.
     *
     * @param tokenCache the token cache
     */
    public RedisTokenStore(RedisTemplate<String, SimpleToken> tokenCache) {
        this.tokenCache = tokenCache;
    }

    /**
     * Instantiates a new Redis token store.
     *
     * @param tokenCache         the token cache
     * @param authorizationStore the authorization store
     */
    public RedisTokenStore(RedisTemplate<String, SimpleToken> tokenCache,
                           AuthorizationStore authorizationStore) {
        this(tokenCache);
        this.tokenCache = tokenCache;
        this.authorizationStore = authorizationStore;
    }

    @Override
    public boolean isSelfExpired() {
        return true;
    }

    @Override
    public SimpleToken saveToken(SimpleToken token) {
        resolveMaxTokens(token.getDetails());
        final HeimdallProperties config = ConfigManager.getConfig();
        final long timeout = config.getToken().getTimeout();
        final int maximumTokens = config.getToken().getMaximumTokens();
        log.debug("[cacheToken]::token = [{}]", token);
        final Boolean aBoolean = tokenCache.opsForValue().setIfAbsent(generateCacheKey(token.getId()), token,
                timeout, TimeUnit.SECONDS
        );
        log.info("[cacheToken]::token = [{}], tokenCacheTimeOut = [{}]", token, timeout);
        //成功了
        if (null != aBoolean && aBoolean) {
            //允许多次登录，且配置了权限缓存 Dao,则清理上次用户的缓存权限
            if (maximumTokens > 1 && null != authorizationStore) {
                authorizationStore.removeUserAuthorities(token.getDetails());
            }
            onCreated(token);
            return token;
        }
        // 没成功
        throw new HeimdallCacheException("Failed to cache token:" + aBoolean);
    }

    @Override
    public SimpleToken getToken(String tokenId) {
        log.debug("[getToken]::tokenId = [{}]", tokenId);
        if (StrUtils.isBlank(tokenId)) {
            return null;
        }
        final SimpleToken simpleToken = tokenCache.opsForValue().get(generateCacheKey(tokenId));
        log.debug("[getToken]::tokenId = [{}],token = [{}]", tokenId, simpleToken);
        onRead(simpleToken);
        return simpleToken;
    }

    @Override
    public SimpleToken update(SimpleToken token) {
        log.debug("[update]::token = [{}]", token);
        final SimpleToken newToken = renewToken(token);
        log.debug("[update]::token = [{}],newToken = [{}]", token, newToken);
        //发布事件
        onUpdated(newToken);
        return newToken;
    }

    /**
     * 续签 token 时长
     *
     * @param token the token
     * @return the simple token
     */
    public SimpleToken renewToken(SimpleToken token) {
        final HeimdallProperties config = ConfigManager.getConfig();
        final boolean renewal = config.getToken().isRenewal();
        final double ratio = config.getToken().getRatio();
        final long timeout = config.getToken().getTimeout();
        log.debug("[renewToken]::token = [{}]", token);
        if (null == token) {
            return null;
        }
        String cacheKey = generateCacheKey(token.getId());
        //要续签
        if (renewal) {
            //看看 token redis 里面还剩下多长时间，也就是 TTL
            final Long expire = tokenCache.getExpire(cacheKey, TimeUnit.SECONDS);
            if (null != expire) {
                //不存在的 key，总之没找到
                if (-2 == expire.intValue()) {
                    //提示一下
                    log.error("[renewToken]::Failed to update token data. Key: {} does not exist in redis. Please check whether the data is correct? ", cacheKey);
                    // throw new HeimdallTokenException();
                }
                //根据情况判断是否需要续签
                //可能是错误修改 key 值或其他原因，反正就是导致 key 的 ttl 变成-1 了，也就是没限制了
                //直接重置到默认全局时长,提示一下。
                if (-1 == expire) {
                    log.warn("[renewToken]:: When renewing token, TTL of session: {} = - 1, which means it will never expire. Reset TTL to: {}", cacheKey, timeout);
                }
                log.debug("[renewToken]::Renewal token, set renewal ratio: {}", ratio);
                //小于0.01按0.01对待
                double radio = Math.max(ratio, 0.01);
                //大于0.99按0.99对待
                radio = Math.min(radio, 0.99);
                log.debug("[renewToken]::Renewal session, actual renewal ratio: {}", radio);
                // 当前剩余时间占比
                final double v = expire.doubleValue() / timeout;
                if (v < radio || -1 == expire) {
                    log.info("[renewToken]::Token key = [{}], current remaining time = [{}] seconds, global expiration time = [{}] seconds, ratio = [{}], " +
                                    "lower than the set value = [{}], renew to global time = [{}] seconds . ",
                            cacheKey, expire.doubleValue(), timeout,
                            v, radio, timeout);
                    //先把 token 自身 exp 延长,再处理 redis里的
                    final SimpleToken newToken = token.renewalToken(token, timeout);
                    //再把延长后的新 token 存入 redis
                    tokenCache.opsForValue().set(cacheKey, newToken, timeout, TimeUnit.SECONDS);
                    return newToken;
                } else {
                    log.debug("[renewToken]::Token  key = [{}], current remaining time = [{}] seconds, global expiration time = [{}] seconds, ratio = [{}]," +
                                    " higher than the set value = [{}], Abort.",
                            cacheKey, expire.doubleValue(), timeout,
                            v, radio);
                }
            }
        } else {
            log.debug("[renewToken]:: token renew is disabled. token = [{}]", token);
        }
        return token;
    }

    @Override
    public Collection<SimpleToken> getPrincipalTokens(String principal) {
        log.debug("[getPrincipalTokens]::principal = [{}]", principal);
        if (StrUtils.isBlank(principal)) {
            return new ArrayList<>();
        }
        List<SimpleToken> principalTokens = new ArrayList<>();
        final Set<String> keys = tokenCache.keys(ConfigManager.getConfig().getToken().getCachePrefix() + "*");
        if (null != keys && !keys.isEmpty()) {
            final List<SimpleToken> simpleTokens = tokenCache.opsForValue().multiGet(keys);
            if (null != simpleTokens && !simpleTokens.isEmpty()) {
                for (SimpleToken simpleToken : simpleTokens) {
                    if (simpleToken.getDetails().getPrincipal().equals(principal)) {
                        principalTokens.add(simpleToken);
                    }
                }
            }
        }
        return principalTokens;
    }

    @Override
    public Collection<SimpleToken> getActiveTokens() {
        log.debug("[getActiveTokens]::");
        final Set<String> keys = tokenCache.keys(ConfigManager.getConfig().getToken().getCachePrefix() + "*");
        if (null != keys && !keys.isEmpty()) {
            final List<SimpleToken> simpleTokens = tokenCache.opsForValue().multiGet(keys);
            return null != simpleTokens && !simpleTokens.isEmpty() ?
                    simpleTokens.stream().sorted(Comparator.comparing(SimpleToken::getIat)
                            .reversed()).collect(Collectors.toList())
                    : null;

        }
        return null;
    }

    @Override
    public Collection<SimpleToken> getActiveTokens(String appId) {
        log.debug("[getActiveTokens]::appId = [{}]", appId);
        if (StrUtils.isBlank(appId)) {
            return new ArrayList<>();
        }
        final Collection<SimpleToken> activeTokens = getActiveTokens();
        List<SimpleToken> tokens = new ArrayList<>();
        if (null != activeTokens && !activeTokens.isEmpty()) {
            for (SimpleToken activeToken : activeTokens) {
                if (activeToken.getDetails().getAppId().equals(appId)) {
                    tokens.add(activeToken);
                }
            }
        }
        return tokens.stream().sorted(Comparator.comparing(SimpleToken::getIat).reversed()).collect(Collectors.toList());
    }

    @Override
    public PageModel<SimpleToken> getActiveTokensPage(int pageNumber, int pageSize) {
        final Collection<SimpleToken> activeTokens = getActiveTokens();
        if (null == activeTokens || activeTokens.isEmpty()) {
            return new PageModel<>();
        }
        return new PageModel<>(activeTokens, pageNumber, pageSize);
//        throw new UnsupportedOperationException("Redis : Not implemented yet");
    }

    @Override
    public PageModel<SimpleToken> getActiveTokensPage(String appId, int pageNumber, int pageSize) {
        final Collection<SimpleToken> activeTokens = getActiveTokens(appId);
        if (null == activeTokens || activeTokens.isEmpty()) {
            return new PageModel<>();
        }
        return new PageModel<>(activeTokens, pageNumber, pageSize);
//        throw new UnsupportedOperationException("Redis : Not implemented yet");
    }

    @Override
    public SimpleToken deleteToken(String tokenId) {
        final SimpleToken token = tokenCache.opsForValue().get(generateCacheKey(tokenId));
        if (null != token) {
            tokenCache.delete(generateCacheKey(token.getId()));
            onDeleted(token);
            //给缓存清理掉
            if (null != authorizationStore) {
                log.debug("[deleteToken]:: clear the UserAuthorities at the same time after remove token, tokenId = [{}]", tokenId);
                authorizationStore.removeUserAuthorities(token.getDetails());
            }
            return token;
        }
        return null;
    }

    @Override
    public int deletePrincipalTokens(String principal) {
        List<String> principalTokenKeys = new ArrayList<>();
        final Set<String> keys = tokenCache.keys(ConfigManager.getConfig().getToken().getCachePrefix() + "*");
        if (null != keys && !keys.isEmpty()) {
            final List<SimpleToken> simpleTokens = tokenCache.opsForValue().multiGet(keys);
            if (null != simpleTokens && !simpleTokens.isEmpty()) {
                for (SimpleToken simpleToken : simpleTokens) {
                    if (simpleToken.getDetails().getPrincipal().equals(principal)) {
                        principalTokenKeys.add(generateCacheKey(simpleToken.getId()));
                    }
                }
            }
        }
        if (!principalTokenKeys.isEmpty()) {
            tokenCache.delete(principalTokenKeys);
            return principalTokenKeys.size();
        }
        return 0;
    }

    @Override
    public int deleteAppTokens(String appId) {
        log.debug("[deleteAppTokens]::appId = [{}]", appId);
        final Collection<SimpleToken> activeTokens = getActiveTokens(appId);
        List<String> deletedKeys = new ArrayList<>();
        if (null != activeTokens && !activeTokens.isEmpty()) {
            for (SimpleToken activeToken : activeTokens) {
                if (activeToken.getDetails().getAppId().equals(appId)) {
                    deletedKeys.add(generateCacheKey(activeToken.getId()));
                }
            }
        }
        if (!deletedKeys.isEmpty()) {
            tokenCache.delete(deletedKeys);
            log.info("[deleteAppTokens]::appId = [{}],deleted count = [{}]", appId, deletedKeys.size());
            return deletedKeys.size();
        }
        log.debug("[deleteAppTokens]::appId = [{}],deleted count =0", appId);
        return 0;
    }

    @Override
    public int clearExpiredTokens() {
        /////啥也不干。redis 自己会清理
        return 0;
    }

    @Override
    public long getActiveTokensCount() {
        final Set<String> keys = tokenCache.keys(ConfigManager.getConfig().getToken().getCachePrefix() + "*");
        return null != keys ? keys.size() : 0;
    }

    /**
     * 产生 token 缓存 key
     *
     * @param tokenId the token id
     * @return the string
     */
    private String generateCacheKey(String tokenId) {
        log.debug("[generateCacheKey]::tokenId = [{}]", tokenId);
        return generateCacheKey(ConfigManager.getConfig().getToken().getCachePrefix(), tokenId);
    }

    /**
     * 构造包含前缀和 唯一凭据 的缓存 key
     * <p>
     * token 缓存 key 格式：
     * <p>
     * 前缀 : principal: token Id
     *
     * @param orgPrefix 设置的前缀
     * @param tokenId   the token id
     * @return 最终缓存 key
     */
    private String generateCacheKey(String orgPrefix, String tokenId) {
        if (StrUtils.isBlank(tokenId)) {
            throw new HeimdallCacheException("TokenId can not be null");
        }
        if (StrUtils.isBlank(orgPrefix)) {
            throw new HeimdallCacheException("TokenId can not be null");
        }
        log.debug("[generateCacheKey]::orgPrefix = [{}], tokenId = [{}]", orgPrefix, tokenId);
        //前后空格和冒号
        String prefix = orgPrefix.trim();
        prefix = prefix.startsWith(StrUtils.COLON) ? prefix.replaceFirst(StrUtils.COLON, StrUtils.EMPTY_STRING) : prefix;
        prefix = prefix.endsWith(StrUtils.COLON) ? prefix : prefix + StrUtils.COLON;
        return prefix + tokenId;
    }

    /**
     * 并发登录处理逻辑
     *
     * @param userDetails the user details
     */
    private void resolveMaxTokens(UserDetails userDetails) {
        final int maximumTokens = ConfigManager.getConfig().getToken().getMaximumTokens();
        log.debug("[resolveMaxTokens]::userDetails = [{}]", userDetails);
        //小于-1 的，提示一下，就当 -1 对待了
        if (maximumTokens < 0) {
//            throw new IllegalArgumentException("maximum tokens can not be less than -1");
            log.warn("[resolveMaxTokens]::Bad parameter settings .maximum tokens is set to {}, " +
                    "The concurrent authentication restriction function will disabled。", maximumTokens);
        }
        //拒绝登录了
        if (0 == maximumTokens) {
            log.warn("[resolveMaxTokens]::maximum tokens is set to {}, The  authentication  function will disabled。", maximumTokens);
            throw new HeimdallTokenException("authentication has been disabled");

        }
        //开启了重复登录限制
        if (maximumTokens > 0) {
            final Collection<SimpleToken> principalTokens = getPrincipalTokens(userDetails.getPrincipal());
            //超过次数了
            if (null != principalTokens && principalTokens.size() >= maximumTokens) {
                if (ConfigManager.getConfig().getToken().isMaxTokensPreventLogin()) {
                    log.warn("[resolveMaxTokens]::userDetails:[{}],   exceeded the maximum Tokens limit of [{}] times . request rejected", userDetails, maximumTokens);
                    throw new HeimdallExceededTokenException();
                } else {
                    //把前面的都踢出去
                    List<String> deleteKeys = new ArrayList<>();
                    for (SimpleToken principalToken : principalTokens) {
                        deleteKeys.add(generateCacheKey(principalToken.getId()));
                    }
                    tokenCache.delete(deleteKeys);
                    log.warn("[resolveMaxTokens]:: Remove all previously authenticated tokens of the " +
                            "User:  userDetails = [{}],count= [{}]", userDetails, deleteKeys.size());
                }

            }
        }
    }

}
